#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

using System;
using System.IO;
#if NETSTANDARD1_3
using System.Reflection;
#endif
using System.Collections;
using log4net.Util;

namespace log4net.ObjectRenderer
{
  /// <summary>
  /// Map class objects to an <see cref="IObjectRenderer"/>.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Maintains a mapping between types that require special
  /// rendering and the <see cref="IObjectRenderer"/> that
  /// is used to render them.
  /// </para>
  /// <para>
  /// The <see cref="M:FindAndRender(object)"/> method is used to render an
  /// <c>object</c> using the appropriate renderers defined in this map.
  /// </para>
  /// </remarks>
  /// <author>Nicko Cadell</author>
  /// <author>Gert Driesen</author>
  public class RendererMap
  {
    private static readonly Type declaringType = typeof(RendererMap);

    #region Member Variables

    private readonly Hashtable m_map = new();
    private readonly Hashtable m_cache = new();

    private static IObjectRenderer s_defaultRenderer = new DefaultRenderer();

    #endregion

    /// <summary>
    /// Render <paramref name="obj"/> using the appropriate renderer.
    /// </summary>
    /// <param name="obj">the object to render to a string</param>
    /// <returns>the object rendered as a string</returns>
    /// <remarks>
    /// <para>
    /// This is a convenience method used to render an object to a string.
    /// The alternative method <see cref="M:FindAndRender(object,TextWriter)"/>
    /// should be used when streaming output to a <see cref="TextWriter"/>.
    /// </para>
    /// </remarks>
    public string FindAndRender(object obj)
    {
      // Optimisation for strings
      string strData = obj as String;
      if (strData != null)
      {
        return strData;
      }

      using StringWriter stringWriter = new StringWriter(System.Globalization.CultureInfo.InvariantCulture);
      FindAndRender(obj, stringWriter);
      return stringWriter.ToString();
    }

    /// <summary>
    /// Render <paramref name="obj"/> using the appropriate renderer.
    /// </summary>
    /// <param name="obj">the object to render to a string</param>
    /// <param name="writer">The writer to render to</param>
    /// <remarks>
    /// <para>
    /// Find the appropriate renderer for the type of the
    /// <paramref name="obj"/> parameter. This is accomplished by calling the
    /// <see cref="M:Get(Type)"/> method. Once a renderer is found, it is
    /// applied on the object <paramref name="obj"/> and the result is returned
    /// as a <see cref="string"/>.
    /// </para>
    /// </remarks>
    public void FindAndRender(object obj, TextWriter writer)
    {
      if (obj == null)
      {
        writer.Write(SystemInfo.NullText);
      }
      else
      {
        // Optimisation for strings
        string str = obj as string;
        if (str != null)
        {
          writer.Write(str);
        }
        else
        {
          // Lookup the renderer for the specific type
          try
          {
            Get(obj.GetType()).RenderObject(this, obj, writer);
          }
          catch (Exception ex)
          {
            // Exception rendering the object
            LogLog.Error(declaringType, "Exception while rendering object of type [" + obj.GetType().FullName + "]", ex);

            // return default message
            string objectTypeName = "";
            if (obj != null && obj.GetType() != null)
            {
              objectTypeName = obj.GetType().FullName;
            }

            writer.Write("<log4net.Error>Exception rendering object type [" + objectTypeName + "]");
            if (ex != null)
            {
              string exceptionText = null;

              try
              {
                exceptionText = ex.ToString();
              }
              catch
              {
                // Ignore exception
              }

              writer.Write("<stackTrace>" + exceptionText + "</stackTrace>");
            }
            writer.Write("</log4net.Error>");
          }
        }
      }
    }

    /// <summary>
    /// Gets the renderer for the specified object type
    /// </summary>
    /// <param name="obj">the object to lookup the renderer for</param>
    /// <returns>the renderer for <paramref name="obj"/></returns>
    /// <remarks>
    /// <param>
    /// Gets the renderer for the specified object type.
    /// </param>
    /// <param>
    /// Syntactic sugar method that calls <see cref="M:Get(Type)"/> 
    /// with the type of the object parameter.
    /// </param>
    /// </remarks>
    public IObjectRenderer Get(Object obj)
    {
      if (obj == null)
      {
        return null;
      }
      else
      {
        return Get(obj.GetType());
      }
    }

    /// <summary>
    /// Gets the renderer for the specified type
    /// </summary>
    /// <param name="type">the type to lookup the renderer for</param>
    /// <returns>the renderer for the specified type</returns>
    /// <remarks>
    /// <para>
    /// Returns the renderer for the specified type.
    /// If no specific renderer has been defined the
    /// <see cref="DefaultRenderer"/> will be returned.
    /// </para>
    /// </remarks>
    public IObjectRenderer Get(Type type)
    {
      if (type == null)
      {
        throw new ArgumentNullException("type");
      }

      IObjectRenderer result = null;

      // Check cache
      result = (IObjectRenderer)m_cache[type];

      if (result == null)
      {
#if NETSTANDARD1_3
        for (Type cur = type; cur != null; cur = cur.GetTypeInfo().BaseType)
#else
        for (Type cur = type; cur != null; cur = cur.BaseType)
#endif
        {
          // Search the type's interfaces
          result = SearchTypeAndInterfaces(cur);
          if (result != null)
          {
            break;
          }
        }

        // if not set then use the default renderer
        if (result == null)
        {
          result = s_defaultRenderer;
        }

        // Add to cache
        lock (m_cache)
        {
          m_cache[type] = result;
        }
      }

      return result;
    }

    /// <summary>
    /// Internal function to recursively search interfaces
    /// </summary>
    /// <param name="type">the type to lookup the renderer for</param>
    /// <returns>the renderer for the specified type</returns>
    private IObjectRenderer SearchTypeAndInterfaces(Type type)
    {
      IObjectRenderer r = (IObjectRenderer)m_map[type];
      if (r != null)
      {
        return r;
      }
      else
      {
        foreach (Type t in type.GetInterfaces())
        {
          r = SearchTypeAndInterfaces(t);
          if (r != null)
          {
            return r;
          }
        }
      }
      return null;
    }

    /// <summary>
    /// Get the default renderer instance
    /// </summary>
    /// <value>the default renderer</value>
    /// <remarks>
    /// <para>
    /// Get the default renderer
    /// </para>
    /// </remarks>
    public IObjectRenderer DefaultRenderer
    {
      get { return s_defaultRenderer; }
    }

    /// <summary>
    /// Clear the map of renderers
    /// </summary>
    /// <remarks>
    /// <para>
    /// Clear the custom renderers defined by using
    /// <see cref="Put"/>. The <see cref="DefaultRenderer"/>
    /// cannot be removed.
    /// </para>
    /// </remarks>
    public void Clear()
    {
      lock (m_map)
      {
        m_map.Clear();
      }

      lock (m_cache)
      {
        m_cache.Clear();
      }
    }

    /// <summary>
    /// Register an <see cref="IObjectRenderer"/> for <paramref name="typeToRender"/>. 
    /// </summary>
    /// <param name="typeToRender">the type that will be rendered by <paramref name="renderer"/></param>
    /// <param name="renderer">the renderer for <paramref name="typeToRender"/></param>
    /// <remarks>
    /// <para>
    /// Register an object renderer for a specific source type.
    /// This renderer will be returned from a call to <see cref="M:Get(Type)"/>
    /// specifying the same <paramref name="typeToRender"/> as an argument.
    /// </para>
    /// </remarks>
    public void Put(Type typeToRender, IObjectRenderer renderer)
    {
      lock (m_cache)
      {
        m_cache.Clear();
      }

      if (typeToRender == null)
      {
        throw new ArgumentNullException("typeToRender");
      }
      if (renderer == null)
      {
        throw new ArgumentNullException("renderer");
      }

      lock (m_map)
      {
        m_map[typeToRender] = renderer;
      }
    }
  }
}
